<?php

/**
 * 发邮件。
 *
 * @author 煤老板 <meok23@sina.com>
 * @date   2016-06-06
 */

namespace ext;

class Smtp
{
    private $_conf = array(
        'nickname' => '',    // 邮箱昵称，没设置就填 e-mail 地址
        'email'    => '',    // 发信人的 e-mail 地址
        'password' => '',    // 发信人的 e-mail 密码
        'host'     => '',    // 发信人的邮箱服务器
        'port'     => 25,
        'helo'     => 'php', // 与服务器打招呼并告知客户端使用的机器名字，可以随便填写
    );

    private $_fp = null;
    private $_lastMessage = ''; // 最后一条命令的返回消息
    public $isAuth = true;      // 是否验证发信人的用户名密码

    public function __construct($conf)
    {
        $this->_conf = array_merge($this->_conf, $conf);
    }

    public function __destruct()
    {
        if (!empty($this->_fp)) {
            fclose($this->_fp);
            unset($this->_fp);
            $this->_fp = null;
        }
    }

    /**
     * 发邮件。
     *
     * @param string $tos // 收件人邮箱，多个则逗号分隔
     * @param string $subject // 邮件的标题
     * @param string $message // 邮件的内容
     * @param string $content_tpe // HTML 类型或者 TXT 类型
     *
     * @return bool
     * @throws \Exception
     */
    public function sendMail($tos, $subject, $message, $content_tpe = 'HTML')
    {
        $nickname = $this->_conf['nickname'];
        $mail_from = $this->_conf['email'];

        // 组装 header 头，注意编码格式
        $header = '';
        if ('HTML' === $content_tpe) {
            // 猜测：换行符只能是 \r\n
            $header .= "Content-Type: text/html; charset=utf-8\r\n";
        } else {
            $header .= "Content-Type: text/plain; charset=utf-8\r\n";
        }
        $header .= "To: {$tos}\r\n";
        $header .= "From: {$nickname}<{$mail_from}>\r\n";
        $header .= "Subject: {$subject}\r\n";

        $tos_arr = explode(',', $tos);
        foreach ($tos_arr as $rcpt_to) {
            // 打开一个网络连接
            $hostname = $this->_conf['host'];
            $port = $this->_conf['port'];
            $this->_fp = fsockopen($hostname, $port, $errno, $errstr, $timeout = ini_get("default_socket_timeout"));
            if (!$this->_fp) {
                $msg = 'fsockopen error';
                $data = json_encode(array(
                    'errno'    => $errno,
                    'errstr'   => $errstr,
                    'hostname' => $hostname,
                    'port'     => $port,
                ));
                $param = json_encode(array(
                    'tos'     => $tos,
                    'subject' => $subject,
                    'message' => htmlspecialchars($message),
                ));
                throw new \Exception("[{$msg}] {$data} {$param}");
            }

            // 执行SMTP指令
            $helo = $this->_conf['helo'];
            if (false === $this->_putCmd("HELO {$helo}\r\n")) {
                return false;
            }

            if ($this->isAuth) {
                $user = base64_encode($this->_conf['email']);
                if (false === $this->_putCmd("AUTH LOGIN {$user}\r\n")) {
                    return false;
                }

                $password = base64_encode($this->_conf['password']);
                if (false === $this->_putCmd("{$password}\r\n")) {
                    return false;
                }
            }

            if (false === $this->_putCmd("MAIL FROM:<{$mail_from}>\r\n")) {
                return false;
            }

            $rcpt_to = trim($rcpt_to);
            if (false === $this->_putCmd("RCPT TO:<{$rcpt_to}>\r\n")) {
                return false;
            }
            if (false === $this->_putCmd("DATA\r\n")) {
                return false;
            }

            fputs($this->_fp, "{$header}\r\n{$message}\r\n.\r\n");

            if (false == $this->_putCmd("QUIT\r\n")) {
                return false;
            }
        }

        return true;
    }

    /**
     * socket 命令
     *
     * @param string $cmd
     *
     * @return bool
     * @throws \Exception
     */
    private function _putCmd($cmd)
    {
        fputs($this->_fp, $cmd);
        $this->_lastMessage = str_replace("/r/n", "", fgets($this->_fp, 512));

        if (1 > preg_match("/^[23]/", $this->_lastMessage)) {
            /* 如果邮件服务器返回的状态码不是2或者3开头，则表明出现错误. */
            // 220 服务就绪(在socket连接成功时，会返回此信息)
            // 221 正在处理
            // 250 请求邮件动作正确，完成(HELO,MAIL FROM,RCPT TO,QUIT指令执行成功会返回此信息)
            // 354 开始发送数据，结束以 .(DATA指令执行成功会返回此信息，客户端应发送信息)
            // 500 语法错误，命令不能识别
            // 550 命令不能执行，邮箱无效
            // 552 中断处理：用户超出文件空间

            throw new \Exception("[{$cmd} error] last message: {$this->_lastMessage}");
        }

        return true;
    }
}
